package bancosys.tec.persist.hibernate;

import java.lang.reflect.Array;
import java.util.Collection;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import org.apache.log4j.Logger;
import org.hibernate.EntityMode;
import org.hibernate.Hibernate;
import org.hibernate.LockMode;
import org.hibernate.NonUniqueObjectException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.type.Type;

import bancosys.tec.persist.Persistable;

/**
 * @created 11/09/2007
 * @author Gustavo Almeida
 */
public class HibernateUtil {
    private static final Logger LOG = Logger.getLogger(HibernateUtil.class);

    /**
     * Reassocia um grafo de objetos à sessão.
     * 
     * @param objeto grafo de objetos
     */
    public void reassociateObject(Persistable objeto) {
        this.doBeforeReassociate(objeto);
        this.reassociateObjectRecursion(objeto.getController().getSessionFactory(), objeto, new IdentityHashMap<Object, Object>(), "this");
    }

    /**
     * hook executado antes de reassociar um objeto e seus objetos associados.
     * 
     * @param objeto Objeto a ser associado.
     */
    protected void doBeforeReassociate(Persistable objeto) {
        // hook executado antes de reassociar um objeto e seus objetos associados.
    }

    /**
     * Recursão usada para reassociar um grafo de objetos à sessão.
     * 
     * @param sessionFactory a factory de sessão.
     * @param original o objeto original.
     * @param objetosPercorridos mapa de objetos percorridos.
     * @param name o nome do objeto.
     */
    private void reassociateObjectRecursion(SessionFactory sessionFactory, Object original,
            IdentityHashMap<Object, Object> objetosPercorridos, String name) {
        if (original == null) {
            LOG.debug(name + " - null");
            return;
        }

        if (!(original instanceof Persistable)) {
            LOG.debug(name + " - not persistable");
            return;
        }
        // se já passamos por esse cara alguma vez
        if (objetosPercorridos.containsKey(original)) {
            LOG.debug(name + " - already locked");
            return;
        }

        Session session = sessionFactory.getCurrentSession();
        if (session.contains(original)) {
            LOG.debug(name + " - already in session");
            return;
        }

        objetosPercorridos.put(original, null);

        if (!Hibernate.isInitialized(original)) {
            LOG.debug(name + " - not initialized");
            return;
        }
        if (((Persistable) original).getPk() == null) {
            LOG.debug(name + " -  transient, " + original.getClass());

            // caso a entidade não esteja persistida, faz reassociate nas propriedades
            this.doReassociateRecursion(sessionFactory, original, objetosPercorridos, name, session);

            return;
        }

        this.lock(original, name, session);

        this.doReassociateRecursion(sessionFactory, original, objetosPercorridos, name, session);
    }

    /**
     * Método que efetivamente reassocia a sessão para os objetos contidos em original.
     * 
     * @param sessionFactory session factory
     * @param original objeto que contem propriedades a serem reassociadas
     * @param objetosPercorridos hash com os objetos que já foram reassociados
     * @param name nome do objeto
     * @param session sessão associada
     */
    private void doReassociateRecursion(SessionFactory sessionFactory, Object original, IdentityHashMap<Object, Object> objetosPercorridos,
            String name,
            Session session) {
        ClassMetadata classMetadata = sessionFactory.getClassMetadata(Hibernate.getClass(original));
        String[] properties = classMetadata.getPropertyNames();

        for (String property : properties) {
            Type type = classMetadata.getPropertyType(property);

            Object resolvedObject = this.narrow(original);

            Object value = classMetadata.getPropertyValue(resolvedObject, property, EntityMode.POJO);

            if (!type.isAssociationType() || value == null) {
                continue;
            }

            String propertyNamePreffix = name + "." + property;
            if (type.isCollectionType()) {
                this.doReassociateCollectionType(sessionFactory, objetosPercorridos, name, session, value, propertyNamePreffix);
            } else {
                this.reassociateObjectRecursion(sessionFactory, value, objetosPercorridos, propertyNamePreffix);
            }
        }
    }

    /**
     * Reassocia uma colecao, verificando se é um array, um {@link Map} ou uma colecao generica.
     * 
     * @param sessionFactory a {@link SessionFactory}
     * @param objetosPercorridos {@link IdentityHashMap} com os objetos ja associados
     * @param name o nome base da propriedade
     * @param session a {@link Session} a qual a colecao deve ser associada
     * @param value o valor. Deve ser uma {@link Type#isCollectionType()}
     * @param propertyNamePreffix o prefixo a ser usado como nome da propriedade
     */
    private void doReassociateCollectionType(SessionFactory sessionFactory, IdentityHashMap<Object, Object> objetosPercorridos,
            String name,
            Session session, Object value, String propertyNamePreffix) {
        if (!Hibernate.isInitialized(value)) {
            LOG.debug(name + " - not initialized");
            return;
        }

        if (value.getClass().isArray()) {
            int length = Array.getLength(value);
            for (int i = 0; i < length; i++) {
                Object arrayValue = Array.get(value, i);
                this.reassociateObjectRecursion(sessionFactory, arrayValue, objetosPercorridos, propertyNamePreffix + "[" + i + "]");
            }
        } else if (value instanceof Map<?, ?>) {
            @SuppressWarnings("unchecked")
            Set<Entry<?, ?>> entrySet = ((Map) value).entrySet();
            int j = 0;
            for (Object object : entrySet) {
                Entry<?, ?> entry = (Entry<?, ?>) object;
                this.reassociateObjectRecursion(sessionFactory, entry.getKey(), objetosPercorridos, propertyNamePreffix + "[ key: " + j
                        + "]");
                this.reassociateObjectRecursion(sessionFactory, entry.getValue(), objetosPercorridos, propertyNamePreffix + "[ value: "
                        + j++
                        + "]");
            }
        } else {
            int j = 0;
            for (Object obj : ((Collection<?>) value)) {
                this.reassociateObjectRecursion(sessionFactory, obj, objetosPercorridos, propertyNamePreffix + "[" + j++ + "]");
            }
        }
    }

    /**
     * Obtém o lock do objeto na sessão.
     * 
     * @param original o objeto.
     * @param name o nome do objeto.
     * @param session a sessão.
     */
    private void lock(Object original, String name, Session session) {
        if (original == null) {
            return;
        }
        if (original instanceof Persistable) {
            this.doBeforeReassociate((Persistable) original);
        }
        LOG.debug(name + " - locked");
        try {
            session.lock(original, LockMode.NONE);
        } catch (NonUniqueObjectException e) {
            LOG.debug("non unique object exception", e);
        }
    }

    /**
     * Resolve o proxy de um objeto se necessario. TODO Em busca de solução mais limpa. Código replicado do BaseDAO.
     * 
     * @param original objeto persistido, pode estar com lazy ou não
     * @return instancia real do objeto.
     */
    private Object narrow(Object original) {
        if (original == null || !(original instanceof HibernateProxy)) {
            return original;
        }
        return ((HibernateProxy) original).getHibernateLazyInitializer().getImplementation();
    }
}